Fail fast on terminal engine-down errors (402/balance exhausted/auth-expired) - #33
Conversation
A Grok Build 402 "usage balance exhausted" incident caused every task on that engine to burn both retry attempts (~5s each) before failing, and all of it landed in the scoreboard as ordinary model failures. - detect_engine_down_reason() classifies worker output (tail-scoped, ~2000 chars) for billing/auth terminal patterns: HTTP 402, "payment required", "balance exhausted", insufficient credits/quota, expired auth/token/ session/api-key, invalid api key. - RingerRunner tracks which engines are down for the run; once one task reveals it, every other task on that engine (running or queued) fails fast with status "engine-down" / verdict ENGINE_DOWN instead of burning a second attempt or a fresh taskdir. - aggregate_model_log_rows / aggregate_model_scoreboard_rows now exclude ENGINE_DOWN-verdict tasks entirely, so a billing outage no longer drags down a model's pass_rate/first_try_pass_rate. - engines/mock_worker.py gains a MOCK_ENGINE_DOWN directive for testing. Deviation from the "verification executes the artifact" invariant: the engine-down path skips running the check entirely (nothing to verify from a billing/auth failure, and PASS is never claimed). Stdin-closed, explicit sandbox mode, and "logs carry raw worker output" are unaffected — the new [ringer.py]-prefixed log lines follow the same convention as existing attempt-lifecycle lines. 🤖 Generated by JeffOS
|
Review verdict: we want this capability, and we want it once. @mlava's #35/#36 solve the sibling problem — spawn-level failures (missing binary, exit 126/127) — with a second run-scoped circuit breaker living in the same two functions as yours. Merging both as written gives the codebase two parallel engine-health mechanisms with contradictory logging conventions, so instead of us picking a winner: @jeffhamons @mlava — would you two put your heads together and design the canonical mechanism? One PR (co-authored, or stacked with clear layering — your call; authorship is preserved either way). Constraints from the maintainers, which are firm:
Fail-fast-on-dead-engine is a real waste-killer and we'd like it in main soon — happy to review a joint design sketch in either thread before you build. Thanks to you both for attacking the same real problem from opposite ends. |
…e-down errors (402/auth) # Conflicts: # ringer.py
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@mlava — sorry for the slow reply, and thanks for the sketch. I'm in, and I think your shape is right: one run-scoped breaker, two detectors, visible-but-unranked rows, single-probe admission. I also agree with your packaging preference — one co-authored PR superseding #33/#35/#36. #35's row-suppression rule dying in favor of visible rows means the stack gets rebuilt from the bottom regardless, so layering would be ceremony. You asked me to correct anything wrong about #33's internals. Four things, and one of them changes the design rather than just the description. 1. There are no structured signals in #33 today — Detector B is content-only. Your sketch says "structured signals where they exist, with Jeff's tail-scoped text patterns as fallback." Worth being explicit that the fallback is currently the whole detector. What exists is a regex table scanned against the last 2000 chars of combined stdout/stderr, gated on 2. The 402 gap is wider than "plain-text Any engine that surfaces the bare status code without the reason phrase is invisible to it. 3. The important one: widening the 402 matchers makes the false-positive problem strictly worse, so the fallback needs the same corroboration treatment you designed for Detector A. The narration case is not hypothetical: That's constraint 4 firing on the current matcher. Two things keep it from being live today: the scan is tail-scoped, and the call site requires a nonzero exit — a worker that narrates about billing and then succeeds can't trip anything. So the exposure is narrower than the raw matcher suggests, but it's real for any task that discusses payment/auth handling and then fails its check for unrelated reasons. And it gets worse the moment we widen So I don't think "narrow the matchers" alone can satisfy both 2 and 4 — the table has to get broader to fix the gap and the decision has to get narrower to fix the false positive. The corroboration signal is already sitting there unused:
That buys us room to widen the patterns to bare status codes without the diff-and-fixture false positives. 4. My PR body overstates in-flight behavior — you're right to claim only "no new spawns, no retries." #33 checks the breaker in exactly two places: before taskdir prep, and at the top of each attempt. A task already inside Related, and worth flagging against your admission gate: #33's end-to-end test runs at 5. Constraint 3 is already failing in #33 as written, and I'd missed it. The maintainers' example of the unforgivable display bug — "a dead-engine run that displays as 'passed' or 'waiting'" — is not hypothetical, it's what my PR does right now: Both new statuses fall through the Proposed split, if it suits you:
Whichever of us opens the PR, co-author trailers for both and it supersedes all three of these. Maintainers — if this shape and split look right, we'll build against it. The one open question I'd want your call on before we start: does the token-corroboration rule in point 3 satisfy constraint 4 for you, given it lets the pattern table get broader than it is today? |
Summary
Fixes an incident (2026-07-12) where a Grok Build engine returning
402 Payment Required: usage balance exhaustedcaused every task in a 5-lane run to burn both retry attempts (~5s each) before failing — 10 wasted worker invocations logged to the scoreboard as ordinary model failures.detect_engine_down_reason()classifies worker stdout/stderr (tail-scoped to the last ~2000 chars, where a terminal harness error actually lands) for billing/auth patterns: HTTP 402 (plain text and JSONstatus/codefields), "payment required", "balance exhausted", insufficient credits/quota, expired auth/token/session/API-key, invalid API key.RingerRunnertracks which engines are down for the run. The first task to hit a terminal error marks its engine down; every other task on that engine — already running or still queued — fails fast with a distinctengine-downstatus /ENGINE_DOWNverdict instead of burning a second attempt or even preparing a taskdir/worktree.aggregate_model_log_rows/aggregate_model_scoreboard_rowsnow excludeENGINE_DOWN-verdict tasks entirely (same pattern as the existing reserved-fixture guard), so a billing outage no longer drags down a model'spass_rate/first_try_pass_rate.engines/mock_worker.pygains aMOCK_ENGINE_DOWNdirective for deterministic testing.Design notes for reviewers
[ringer.py]-prefixed log lines follow the same convention as the existing attempt-lifecycle lines.docs/MODEL-NOTES.mdandengines/opencode-sandboxed.shuntouched (other owners' files per repo convention).Test plan
tests/test_engine_down.py: pattern-matching unit tests (positive/negative cases including the exact incident string), an aggregation-exclusion unit test, and a full subprocess end-to-end test (3 tasks,max_parallel=1) proving task-one burns exactly 1 attempt and task-two/three never even get a taskdir.git stashthat they fail identically on the unmodified baseline.ruff checkon changed files: no new findings.